#!/usr/bin/env bash
set -eo pipefail
[[ $TRACE ]] && set -x

readonly ROOT_DIR="$(cd "$(dirname "$(dirname "${BASH_SOURCE[0]}")")" && pwd)"
readonly TMP_WORK_DIR="$(mktemp -d "/tmp/dokku-release.XXXXXX")"
readonly DOKKU_GIT_REV="$(git rev-parse HEAD)"

trap "rm -rf '$TMP_WORK_DIR' >/dev/null" RETURN INT TERM EXIT

log-info() {
  declare desc="Log info formatter"
  echo "$*"
}

log-error() {
  declare desc="Log error formatter"
  echo "!   $*" 1>&2
}

log-fail() {
  declare desc="Log fail formatter"
  log-error "$*"
  exit 1
}

fn-build-dokku() {
  declare desc="Builds dokku packages within a docker container"
  declare IS_RELEASE="$1" DOKKU_VERSION="$2"
  local NAME GOLANG_VERSION

  if [[ -z "$DOKKU_VERSION" ]]; then
    log-error "Invalid deb file specified"
    return 1
  fi

  NAME="dokku:build"
  [[ "$IS_RELEASE" == "true" ]] && NAME="dokku:build-release"
  [[ -n "$DOKKU_VERSION" ]] || DOKKU_VERSION="master"

  pushd "$ROOT_DIR" >/dev/null
  GOLANG_VERSION="$(grep BUILD_IMAGE common.mk | cut -d' ' -f3 | cut -d':' -f2)"
  docker build \
    --build-arg DOKKU_VERSION="$DOKKU_VERSION" \
    --build-arg DOKKU_GIT_REV="$DOKKU_GIT_REV" \
    --build-arg IS_RELEASE="$IS_RELEASE" \
    --build-arg PLUGIN_MAKE_TARGET="build" \
    --build-arg GOLANG_VERSION="$GOLANG_VERSION" \
    --progress plain \
    -f "contrib/build-dokku.Dockerfile" \
    -t "$NAME" .
  return "$?"
}

fn-extract-package() {
  declare desc="Extract packages from a docker container to the root directory"
  declare IS_RELEASE="$1" PACKAGE_NAME="$2"
  local NAME

  if [[ -z "$PACKAGE_NAME" ]]; then
    log-error "Invalid deb file specified"
    return 1
  fi

  NAME="dokku:build"
  [[ "$IS_RELEASE" == "true" ]] && NAME="dokku:build-release"

  log-info "(extract-package) writing ${PACKAGE_NAME} to correct path"
  docker run --rm --entrypoint cat "$NAME" "/tmp/${PACKAGE_NAME}" >"${ROOT_DIR}/build/${PACKAGE_NAME}"
  return "$?"
}

fn-in-array() {
  declare desc="return true if value ($1) is in list (all other arguments)"

  local e
  for e in "${@:2}"; do
    [[ "$e" == "$1" ]] && return 0
  done
  return 1
}

fn-is-release() {
  declare desc="Checks if a given run is a release run"
  declare RELEASE="$1"
  local IS_RELEASE=false
  if [[ "$RELEASE" == "major" ]] || [[ "$RELEASE" == "minor" ]] || [[ "$RELEASE" == "patch" ]]; then
    IS_RELEASE=true
  fi

  echo "$IS_RELEASE"
}

fn-version-current() {
  declare desc="Retrieves the current version of dokku"
  pushd "$ROOT_DIR" >/dev/null
  grep Version debian/control | cut -d' ' -f2
}

fn-version-next() {
  declare desc="Calculates the next version name of dokku"
  declare CURRENT_VERSION="$1" RELEASE="$2"
  local major minor patch short_object

  major=$(echo "$CURRENT_VERSION" | awk '{split($0,a,"."); print a[1]}')
  minor=$(echo "$CURRENT_VERSION" | awk '{split($0,a,"."); print a[2]}')
  patch=$(echo "$CURRENT_VERSION" | awk '{split($0,a,"."); print a[3]}')

  if [[ "$RELEASE" == "major" ]]; then
    major=$((major + 1))
    minor="0"
    patch="0"
  elif [[ "$RELEASE" == "minor" ]]; then
    minor=$((minor + 1))
    patch="0"
  elif [[ "$RELEASE" == "patch" ]]; then
    patch=$((patch + 1))
  else
    short_object=$(git log --oneline | tail -n 1 | cut -d' ' -f1)
    echo "${CURRENT_VERSION}build+$(echo "$DOKKU_GIT_REV" | cut -c1-${#short_object})"
    return 0
  fi

  echo "${major}.${minor}.${patch}"
}

fn-publish-package() {
  declare desc="Publishes a package to packagecloud"
  declare IS_RELEASE="$1" RELEASE_TYPE="$2" PACKAGE_NAME="$3"
  local REPOSITORY=dokku/dokku-betafish DIST=ubuntu EXIT_CODE=0
  local OS_ID ex

  [[ "$IS_RELEASE" == "true" ]] && REPOSITORY=dokku/dokku
  [[ "$RELEASE_TYPE" == "raspbian" ]] && DIST=raspbian

  if [[ "$RELEASE_TYPE" == "raspbian" ]]; then
    OS_IDS=("bullseye" "bookworm" "trixie")
    for OS_ID in "${OS_IDS[@]}"; do
      log-info "(release-dokku) pushing deb to packagecloud.com/${REPOSITORY}/${DIST}"
      package_cloud push "${REPOSITORY}/${DIST}/${OS_ID}" "$PACKAGE_NAME"
      ex="$?"
      if [[ "$ex" -ne "0" ]]; then
        EXIT_CODE="$ex"
      fi
    done
  elif [[ "$DIST" == "ubuntu" ]]; then
    OS_IDS=("jammy" "noble")
    for OS_ID in "${OS_IDS[@]}"; do
      log-info "(release-dokku) pushing ${RELEASE_TYPE} to packagecloud.com/${REPOSITORY}/${DIST}"
      package_cloud push "${REPOSITORY}/${DIST}/${OS_ID}" "$PACKAGE_NAME"
      ex="$?"
      if [[ "$ex" -ne "0" ]]; then
        EXIT_CODE="$ex"
      fi
    done

    DIST=debian
    OS_IDS=("bullseye" "bookworm" "trixie")
    for OS_ID in "${OS_IDS[@]}"; do
      log-info "(release-dokku) pushing ${RELEASE_TYPE} to packagecloud.com/${REPOSITORY}/${DIST}"
      package_cloud push "${REPOSITORY}/${DIST}/${OS_ID}" "$PACKAGE_NAME"
      ex="$?"
      if [[ "$ex" -ne "0" ]]; then
        EXIT_CODE="$ex"
      fi
    done
  else
    log-info "(release-dokku) pushing ${RELEASE_TYPE} to packagecloud.com/${REPOSITORY}/${DIST}"
    package_cloud push "${REPOSITORY}/${DIST}" "$PACKAGE_NAME"
    EXIT_CODE="$?"
  fi
  return "$EXIT_CODE"
}

fn-replace-version() {
  declare desc="Replaces the version within a file"
  local CURRENT_VERSION="$1"
  local NEXT_VERSION="$2"
  local FILENAME="$3"
  sed -i.bak "s/${CURRENT_VERSION//./\.}/$NEXT_VERSION/g" "$FILENAME" && rm "$FILENAME.bak"
  git add "$FILENAME"
}

fn-repo-merged-requests() {
  declare desc="Retrieves all pull request ids since a given release of dokku"
  declare CURRENT_VERSION="$1" NEXT_VERSION="$2" IS_PATCH="$3"
  if [[ "$IS_PATCH" == "true" ]]; then
    git log "v${CURRENT_VERSION}..HEAD" --oneline | grep "Merge pull request" | cut -d' ' -f 5 | cut -d '#' -f2
  else
    curl -u "$RELEASE_GITHUB_USERNAME:$RELEASE_GITHUB_API_TOKEN" --fail -sS "https://api.github.com/search/issues?q=repo:dokku/dokku+milestone:v${NEXT_VERSION}+is:pr&per_page=100" | jq '.items[].number'
  fi
}

fn-repo-push-tags() {
  declare desc="Pushes tags and the repository to github"
  declare IS_RELEASE="$1"

  if [[ "$IS_RELEASE" == "false" ]]; then
    return
  fi

  pushd "$ROOT_DIR" >/dev/null
  git push --tags origin master
}

fn-repo-update() {
  declare desc="Updates files within the repository for a given release"
  declare IS_RELEASE="$1" CURRENT_VERSION="$2" NEXT_VERSION="$3" IS_PATCH="$4"

  log-info "(release-dokku) Replacing ${CURRENT_VERSION} with ${NEXT_VERSION}"
  for f in plugins/*/plugin.toml; do
    fn-replace-version "$CURRENT_VERSION" "$NEXT_VERSION" "$f"
  done

  if [[ "$IS_RELEASE" == "false" ]]; then
    return
  fi

  fn-replace-version "$CURRENT_VERSION" "$NEXT_VERSION" debian/control
  fn-replace-version "$CURRENT_VERSION" "$NEXT_VERSION" Dockerfile
  fn-replace-version "$CURRENT_VERSION" "$NEXT_VERSION" docs/advanced-usage/plugin-management.md
  fn-replace-version "$CURRENT_VERSION" "$NEXT_VERSION" docs/assets/favicons/browserconfig.xml
  fn-replace-version "$CURRENT_VERSION" "$NEXT_VERSION" docs/assets/favicons/manifest.json
  fn-replace-version "$CURRENT_VERSION" "$NEXT_VERSION" docs/assets/style.css
  fn-replace-version "$CURRENT_VERSION" "$NEXT_VERSION" docs/development/release-process.md
  fn-replace-version "$CURRENT_VERSION" "$NEXT_VERSION" docs/getting-started/install/docker.md
  fn-replace-version "$CURRENT_VERSION" "$NEXT_VERSION" docs/getting-started/installation/index.md
  fn-replace-version "v$CURRENT_VERSION" "v$NEXT_VERSION" docs/home.html
  fn-replace-version "$CURRENT_VERSION" "$NEXT_VERSION" docs/template.html
  fn-replace-version "$CURRENT_VERSION" "$NEXT_VERSION" README.md

  if [[ "$RELEASE" == 'patch' ]]; then
    fn-replace-version "$CURRENT_VERSION" "$NEXT_VERSION" docs/assets/versions.json
  else
    versions=$(python3 -c 'import json,sys;d=json.load(sys.stdin);d["max-versions"].append("'"${NEXT_VERSION}"'"); print(json.dumps(d, indent=2, sort_keys=True))' <docs/assets/versions.json)
    echo "$versions" >docs/assets/versions.json
    git add docs/assets/versions.json
  fi

  if [[ "$IS_RELEASE" == "false" ]]; then
    return
  fi

  log-info "(release-dokku) Adding HISTORY.md, committing and tagging"
  fn-repo-update-history-and-commit "$CURRENT_VERSION" "$NEXT_VERSION" "$IS_PATCH"
  git tag "v${NEXT_VERSION}"
}

fn-repo-update-history-and-commit() {
  declare desc="Updates the history file and commits the changes"
  declare CURRENT_VERSION="$1" NEXT_VERSION="$2" IS_PATCH="$3"
  local PULL_REQUEST_ID TITLE AUTHOR TYPE CHANGELOG_TEXT
  local COMMIT_MESSAGE HISTORY HISTORY_CONTENTS HISTORY_BUG HISTORY_DEPENDENCY HISTORY_DEPRECATION HISTORY_DOCUMENTATION HISTORY_ENHANCEMENT HISTORY_BC_BREAK HISTORY_REFACTOR HISTORY_REMOVAL HISTORY_OTHER HISTORY_TESTS ISSUE_FILE

  pushd "$ROOT_DIR" >/dev/null

  mkdir -p /tmp/github-data

  for PULL_REQUEST_ID in $(fn-repo-merged-requests "$CURRENT_VERSION" "$NEXT_VERSION" "$IS_PATCH"); do
    ISSUE_FILE="/tmp/github-data/${PULL_REQUEST_ID}.json"
    if [[ ! -f "$ISSUE_FILE" ]]; then
      while true; do
        if OUTPUT=$(curl -u "$RELEASE_GITHUB_USERNAME:$RELEASE_GITHUB_API_TOKEN" --fail -sS "https://api.github.com/repos/dokku/dokku/issues/${PULL_REQUEST_ID}" 2>&1); then
          echo "$OUTPUT" >"$ISSUE_FILE"
          break
        fi

        log-error "Error while fetching info for pull/$PULL_REQUEST_ID, sleeping for 5 seconds"
        log-error "$OUTPUT"
        sleep 5
        log-info "    Retrying pull/$PULL_REQUEST_ID"
      done
    fi

    TITLE="$(jq -r '.title' "$ISSUE_FILE")"
    AUTHOR="$(jq -r '.user.login' "$ISSUE_FILE")"
    TYPE="$(jq -r '.labels[] | select( .name | contains("type") ) | .name' "$ISSUE_FILE" | cut -d' ' -f2)"
    CHANGELOG_TEXT="- #${PULL_REQUEST_ID}: @${AUTHOR} ${TITLE}"

    if [[ "$TYPE" == "release" ]]; then
      continue
    elif [[ "$TYPE" == "bc-break" ]]; then
      HISTORY_BC_BREAK="${HISTORY_BC_BREAK}"$'\n'"$CHANGELOG_TEXT"
    elif [[ "$TYPE" == "bug" ]]; then
      HISTORY_BUG="${HISTORY_BUG}"$'\n'"$CHANGELOG_TEXT"
    elif [[ "$TYPE" == "dependencies" ]]; then
      HISTORY_DEPENDENCY="${HISTORY_DEPENDENCY}"$'\n'"$CHANGELOG_TEXT"
    elif [[ "$TYPE" == "deprecation" ]]; then
      HISTORY_DEPRECATION="${HISTORY_DEPRECATION}"$'\n'"$CHANGELOG_TEXT"
    elif [[ "$TYPE" == "documentation" ]]; then
      HISTORY_DOCUMENTATION="${HISTORY_DOCUMENTATION}"$'\n'"$CHANGELOG_TEXT"
    elif [[ "$TYPE" == "enhancement" ]]; then
      HISTORY_ENHANCEMENT="${HISTORY_ENHANCEMENT}"$'\n'"$CHANGELOG_TEXT"
    elif [[ "$TYPE" == "refactor" ]]; then
      HISTORY_REFACTOR="${HISTORY_REFACTOR}"$'\n'"$CHANGELOG_TEXT"
    elif [[ "$TYPE" == "removal" ]]; then
      HISTORY_REMOVAL="${HISTORY_REMOVAL}"$'\n'"$CHANGELOG_TEXT"
    elif [[ "$TYPE" == "tests" ]]; then
      HISTORY_TESTS="${HISTORY_TESTS}"$'\n'"$CHANGELOG_TEXT"
    else
      HISTORY_OTHER="${HISTORY_OTHER}"$'\n'"$CHANGELOG_TEXT"
    fi
  done

  HISTORY="# History"
  HISTORY="${HISTORY}"$'\n\n'"## ${NEXT_VERSION}"
  HISTORY="${HISTORY}"$'\n\n'"Install/update via the bootstrap script:"
  HISTORY="${HISTORY}"$'\n\n'"\`\`\`shell"
  HISTORY="${HISTORY}"$'\n'"wget -NP . https://dokku.com/install/v${NEXT_VERSION}/bootstrap.sh"
  HISTORY="${HISTORY}"$'\n'"sudo DOKKU_TAG=v${NEXT_VERSION} bash bootstrap.sh"
  HISTORY="${HISTORY}"$'\n'"\`\`\`"

  if [[ -f "docs/appendices/${NEXT_VERSION}-migration-guide.md" ]]; then
    HISTORY="${HISTORY}"$'\n\n'"See the [${NEXT_VERSION} migration guide](/docs/appendices/${NEXT_VERSION}-migration-guide.md) for more information on migrating to ${NEXT_VERSION}."
  fi

  if [[ "$HISTORY_BC_BREAK" ]]; then
    HISTORY="${HISTORY}"$'\n\n'"### Backwards Compatibility Breaks"
    HISTORY="${HISTORY}"$'\n'"$HISTORY_BC_BREAK"
  fi

  if [[ "$HISTORY_BUG" ]]; then
    HISTORY="${HISTORY}"$'\n\n'"### Bug Fixes"
    HISTORY="${HISTORY}"$'\n'"$HISTORY_BUG"
  fi

  if [[ "$HISTORY_ENHANCEMENT" ]]; then
    HISTORY="${HISTORY}"$'\n\n'"### New Features"
    HISTORY="${HISTORY}"$'\n'"$HISTORY_ENHANCEMENT"
  fi

  if [[ "$HISTORY_REMOVAL" ]]; then
    HISTORY="${HISTORY}"$'\n\n'"### Removals"
    HISTORY="${HISTORY}"$'\n'"$HISTORY_REMOVAL"
  fi

  if [[ "$HISTORY_DEPRECATION" ]]; then
    HISTORY="${HISTORY}"$'\n\n'"### Deprecations"
    HISTORY="${HISTORY}"$'\n'"$HISTORY_DEPRECATION"
  fi

  if [[ "$HISTORY_REFACTOR" ]]; then
    HISTORY="${HISTORY}"$'\n\n'"### Refactors"
    HISTORY="${HISTORY}"$'\n'"$HISTORY_REFACTOR"
  fi

  if [[ "$HISTORY_DOCUMENTATION" ]]; then
    HISTORY="${HISTORY}"$'\n\n'"### Documentation"
    HISTORY="${HISTORY}"$'\n'"$HISTORY_DOCUMENTATION"
  fi

  if [[ "$HISTORY_TESTS" ]]; then
    HISTORY="${HISTORY}"$'\n\n'"### Tests"
    HISTORY="${HISTORY}"$'\n'"$HISTORY_TESTS"
  fi

  if [[ "$HISTORY_DEPENDENCY" ]]; then
    HISTORY="${HISTORY}"$'\n\n'"### Dependencies"
    HISTORY="${HISTORY}"$'\n'"$HISTORY_DEPENDENCY"
  fi

  if [[ "$HISTORY_OTHER" ]]; then
    HISTORY="${HISTORY}"$'\n\n'"### Other"
    HISTORY="${HISTORY}"$'\n'"$HISTORY_OTHER"
  fi

  sed -i.bak '1d' HISTORY.md && rm HISTORY.md.bak
  HISTORY_CONTENTS="$(cat HISTORY.md)"
  echo -e "$HISTORY\n$HISTORY_CONTENTS" >HISTORY.md
  git add HISTORY.md

  COMMIT_MESSAGE="Release ${NEXT_VERSION}"$'\n\n'"${HISTORY}"
  git commit -m "${COMMIT_MESSAGE}"
}

fn-require-bin() {
  declare desc="Checks that a binary exists"
  declare BINARY="$1"
  if ! command -v "$BINARY" &>/dev/null; then
    log-fail "Missing ${BINARY}, please install it"
  fi
}

fn-build-docker-image() {
  declare desc="Builds the dokku docker image and tags it with the given version"
  declare VERSION="$1" IS_RELEASE="$2"

  if [[ "$IS_RELEASE" == "true" ]]; then
    docker buildx build \
      --platform linux/arm64,linux/amd64 \
      --progress plain \
      --push \
      --label "org.opencontainers.image.version=$VERSION" \
      --tag "dokku/dokku:latest" \
      --tag "dokku/dokku:$VERSION" .
  elif [[ "$SKIP_IMAGE_BUILD" != "true" ]]; then
    docker buildx build \
      --platform linux/arm64,linux/amd64 \
      --progress plain \
      --label "org.opencontainers.image.version=$VERSION" \
      --tag "dokku/dokku:latest" \
      --tag "dokku/dokku:$(echo "$VERSION" | tr + -)" .
  fi
}

main() {
  declare RELEASE="$1"
  local CURRENT_VERSION NEXT_VERSION IS_PATCH IS_RELEASE
  local VALID_RELEASE_LEVELS=("major" "minor" "patch" "betafish" "build")

  if [[ "$RELEASE" == '--trace' ]]; then
    shift 1
    RELEASE="$1"
    TRACE=1 && set -x
  fi

  if [[ -z "$RELEASE" ]]; then
    log-fail "Argument 1 must be one of [major, minor, patch, betafish, build], none given"
  fi

  if ! fn-in-array "$RELEASE" "${VALID_RELEASE_LEVELS[@]}"; then
    log-fail "Argument 1 must be one of [major, minor, patch, betafish, build], '${RELEASE}' given"
  fi

  log-info "Creating $RELEASE release"

  mkdir -p "build"
  fn-require-bin "docker"
  fn-require-bin "git"
  if [[ "$RELEASE" != "build" ]]; then
    fn-require-bin "package_cloud"
    [[ -n "$PACKAGECLOUD_TOKEN" ]] || log-fail "Missing PACKAGECLOUD_TOKEN environment variable"
  fi

  CURRENT_VERSION="$(fn-version-current)"
  NEXT_VERSION="$(fn-version-next "$CURRENT_VERSION" "$RELEASE")"
  IS_RELEASE="$(fn-is-release "$RELEASE")"

  IS_PATCH="false"
  if [[ "$RELEASE" == "patch" ]]; then
    IS_PATCH="true"
  fi

  echo "v${NEXT_VERSION}" >build/next-version

  fn-repo-update "$IS_RELEASE" "$CURRENT_VERSION" "$NEXT_VERSION" "$IS_PATCH"
  fn-build-dokku "$IS_RELEASE" "$NEXT_VERSION" || log-fail "Error building package"
  fn-extract-package "$IS_RELEASE" "dokku_${NEXT_VERSION}_arm64.deb" || log-fail "Error extracting deb package"
  fn-extract-package "$IS_RELEASE" "dokku_${NEXT_VERSION}_amd64.deb" || log-fail "Error extracting deb package"
  mkdir -p build/package
  cp -f "build/dokku_${NEXT_VERSION}_amd64.deb" build/package/dokku-amd64.deb
  cp -f "build/dokku_${NEXT_VERSION}_arm64.deb" build/package/dokku-arm64.deb

  if [[ "$RELEASE" != "build" ]]; then
    fn-publish-package "$IS_RELEASE" "deb" "build/dokku_${NEXT_VERSION}_arm64.deb" || log-fail "Error publishing deb package"
    fn-publish-package "$IS_RELEASE" "deb" "build/dokku_${NEXT_VERSION}_amd64.deb" || log-fail "Error publishing deb package"
    fn-build-docker-image "$NEXT_VERSION" "$IS_RELEASE" || log-fail "Error building docker image"

    fn-repo-push-tags "$IS_RELEASE"
  else
    fn-build-docker-image "$NEXT_VERSION" "$IS_RELEASE" || log-fail "Error building docker image"
  fi

  if [[ "$IS_RELEASE" != "true" ]]; then
    git reset -q HEAD plugins/*/plugin.toml
    git checkout -- plugins/*/plugin.toml
  fi
}

main "$@"
